﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Extensions.Options;

using Newtonsoft.Json;

using StackExchange.Redis;

namespace Jack.RedisEventBus
{
    /// <summary>
    /// 事件发布者
    /// </summary>
    internal class DefaultEventPublisher : IEventPublisher, IDisposable
    {
        private readonly IOptions<EventBusOption> _options;
        private readonly string _eventChannel;
        private readonly JsonSerializerSettings _jsonSerializerSettings;
        private readonly SemaphoreSlim _connectionLock;
        private volatile ConnectionMultiplexer _connection;

        public DefaultEventPublisher(IOptions<EventBusOption> options)
        {
            _options = options;
            _eventChannel = $"{options.Value.InstanceName}{options.Value.EventChannel}";
            _jsonSerializerSettings = new JsonSerializerSettings
            {
                //DateFormatHandling = DateFormatHandling.IsoDateFormat,
                //DateParseHandling = DateParseHandling.DateTime,
                DefaultValueHandling = DefaultValueHandling.Ignore,
                //FloatFormatHandling = FloatFormatHandling.String,
                NullValueHandling = NullValueHandling.Ignore,
                DateFormatString = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fff",
                //MissingMemberHandling = MissingMemberHandling.Ignore,
                //StringEscapeHandling = StringEscapeHandling.Default,
            };
            _connectionLock = new SemaphoreSlim(1, 1);
        }

        public void Dispose()
        {
            if (_connection != null && _connection.IsConnected)
            {
                _connection.Dispose();
            }
            _connectionLock.Dispose();
        }

        public async Task Publish<TEvent>(TEvent @event, int db = -1, CancellationToken cancellationToken = default) where TEvent : IEvent
        {
            EventData eventData = new EventData
            {
                Data = JsonConvert.SerializeObject(@event, _jsonSerializerSettings),
            };
            Type eventType = typeof(TEvent);
            if (eventType.IsDefined(typeof(EventNameAttribute), false))
            {
                eventData.EventType = eventType.GetCustomAttribute<EventNameAttribute>().Name;
            }
            else
            {
                eventData.EventType = eventType.FullName;
            }

            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            await database.ListLeftPushAsync(_eventChannel, (string)eventData);
        }

        public async Task Publish<TEvent>(IEnumerable<TEvent> @events, int db = -1, CancellationToken cancellationToken = default) where TEvent : IEvent
        {
            Type eventType = typeof(TEvent);
            string strEventType = eventType.FullName;
            if (eventType.IsDefined(typeof(EventNameAttribute), false))
            {
                strEventType = eventType.GetCustomAttribute<EventNameAttribute>().Name;
            }

            RedisValue[] eventDatas = @events.Select(x => (RedisValue)(string)new EventData 
            {
                EventType = strEventType,
                Data = JsonConvert.SerializeObject(x, _jsonSerializerSettings),
            }).ToArray();

            IDatabase database = await GetConnectedDatabase(db, cancellationToken);
            await database.ListLeftPushAsync(_eventChannel, eventDatas);
        }

        private async Task<IDatabase> GetConnectedDatabase(int db = -1, CancellationToken cancellationToken = default)
        {
            if (_connection == null || _connection.IsConnected == false)
            {
                try
                {
                    await _connectionLock.WaitAsync(cancellationToken);
                    if (_connection == null || _connection.IsConnected == false)
                    {
                        _connection = await ConnectionMultiplexer.ConnectAsync(_options.Value.ConnectionString);
                    }
                }
                catch (Exception)
                {
                    throw;
                }
                finally
                {
                    _connectionLock.Release();
                }
            }

            db = db > -1 ? db : _options.Value.Database;
            return _connection.GetDatabase(db);
        }
    }
}
